import functools
import json
import math
import time
from typing import Optional, Type, Callable, Awaitable, Dict
from urllib.parse import urljoin
from pydantic import BaseModel
import httpx

from .crypto import encode, decode, pickcode2bytes, generate_key
from .schemas import (
    UploadInfo, DirList, OperateResp, DownloadInfoEnc, DownloadInfo, TokenInfo, UploadToken
)

RETRY_TIMES: int = 3


class ClientError(Exception):
    pass


class DirNotFound(Exception):
    pass


def parse_cookie(s: str) -> Dict[str, str]:
    c = dict(
        map(lambda x: x.split("="), s.replace(" ", "").split(";"))
    )
    return c


def retry_wrapper(retry_time: int = 3):
    def deco_func(f: Callable[..., Awaitable]):
        @functools.wraps(f)
        async def inner(*args, **kwargs):
            exc: Optional[Exception] = None
            req_time = 0
            while req_time <= retry_time:
                try:
                    return await f(*args, **kwargs)
                except Exception as e:
                    exc = e
                req_time += 1
            raise exc

        return inner

    return deco_func


class ClientFor115(object):
    DIR_URL = "https://webapi.115.com/"  # 目录操作URL
    DOWNLOAD_INFO_URL = "http://proapi.115.com/app/chrome/downurl"  # 获取下载信息的URL
    UPLOAD_INFO_URL = "http://proapi.115.com/app/uploadinfo"  # 上传信息
    UPLOAD_TOKEN_URL = "https://uplb.115.com/3.0/getuploadinfo.php"  # 上传TOKEN

    DEFAULT_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " \
                    "Chrome/106.0.0.0 Safari/537.36"
    list_param = {
        "cid": None,
        "aid": 1,
        "offset": 0,
        "limit": 32,
        "show_dir": 1
    }

    def __init__(
            self, root_id: str, cookies: str, async_client: httpx.AsyncClient,
            sync_client: httpx.Client, timeout: float = 5.0
    ):
        self.root_id = root_id
        self.cookies = cookies
        self.async_client = async_client
        self.sync_client = sync_client
        self.user_id: str = parse_cookie(self.cookies)['UID'].split("_")[0]
        self.timeout = timeout

        self.upload_info: Optional[UploadInfo] = None

        for client in (self.async_client, self.sync_client):
            client.headers.update(
                {
                    "Cookie": self.cookies,
                    "User-Agent": self.DEFAULT_AGENT
                }
            )

    @retry_wrapper(RETRY_TIMES)
    async def do_request(
            self, sub_url: str, method: str, schema: Type[BaseModel], content: bytes = None,
            headers: dict = None, params: dict = None, data: dict = None, base_url: str = None
    ):
        url = urljoin(self.DIR_URL, sub_url) if not base_url else urljoin(base_url, sub_url)

        r = await self.async_client.request(
            method=method, url=url, content=content, headers=headers,
            params=params, data=data, timeout=self.timeout
        )
        if r.status_code == 200:
            r_data = r.json()
            try:
                # 无论resp的state的取值如何均返回
                resp = schema(**r_data)
                return resp
            except Exception:
                raise ClientError()
        else:
            raise ClientError()

    async def list_dir(self, cid: str, **query_param) -> DirList:
        params = {**self.list_param, "cid": cid, **query_param}
        # 当cid不存在时api默认返回用户根目录下文件
        res: DirList = await self.do_request(
            sub_url="files", method="GET", params=params, schema=DirList
        )
        path = res.path
        if not path or path[-1].cid != int(cid):
            raise DirNotFound

        return res

    async def mkdir(self, pid: str, cname: str) -> OperateResp:
        data = {"pid": pid, "cname": cname}
        res = await self.do_request(
            sub_url="files/add", method="POST", data=data,
            headers={"content-type": "application/x-www-form-urlencoded"}, schema=OperateResp
        )
        return res

    async def move(self, pid: str, fid: str) -> OperateResp:
        data = {"pid": pid, "fid[0]": fid, "move_proid": ""}
        res = await self.do_request(
            sub_url="files/move", method="POST", data=data, schema=OperateResp,
            headers={"content-type": "application/x-www-form-urlencoded"}
        )
        return res

    async def rename(self, res_id: str, new_name: str) -> OperateResp:
        data = {f"files_new_name[{res_id}]": new_name}
        res = await self.do_request(
            sub_url="files/batch_rename", method="POST", data=data, schema=OperateResp,
            headers={"content-type": "application/x-www-form-urlencoded"}
        )
        return res

    async def copy(self, pid: str, fid: str) -> OperateResp:
        data = {"pid": pid, "fid[0]": fid}
        res = await self.do_request(
            sub_url="files/copy", method="POST", data=data, schema=OperateResp,
            headers={"content-type": "application/x-www-form-urlencoded"}
        )
        return res

    async def delete_res(self, pid: str, fid: str) -> OperateResp:
        data = {"pid": pid, "fid[0]": fid}
        res = await self.do_request(
            sub_url="rb/delete", method="POST", data=data, schema=OperateResp,
            headers={"content-type": "application/x-www-form-urlencoded"}
        )
        return res

    async def get_download_info(self, pickcode: str):
        """
        根据pickcode获取文件下载信息
        :param pickcode:
        :return:
        """
        rand_key = generate_key()
        input_data = pickcode2bytes(pickcode)
        data = encode(input_data, rand_key)
        payload = {"data": data}
        headers = {"content-type": "application/x-www-form-urlencoded"}
        params = {"t": str(math.floor(int(time.time())))}
        enc_info: DownloadInfoEnc = await self.do_request(
            sub_url='', base_url=self.DOWNLOAD_INFO_URL, data=payload,
            headers=headers, params=params, schema=DownloadInfoEnc, method="POST"
        )
        dec_raw: dict = json.loads(decode(enc_info.data, rand_key))
        dec_info = DownloadInfo(**list(dec_raw.values())[0])
        if not dec_info.url:
            raise ClientError("无法下载该资源, 该资源可能为文件夹")

        return dec_info

    async def get_download_resp(
            self, pickcode: str, range_header: str, sync: bool = True,
            head_only: bool = False
    ) -> httpx.Response:
        """
        返回httpx.Response对象用于下载文件.当head_only为True时, 对目标资源进行HEAD请求,
        否则对目标资源进行GET请求, 返回流式响应
        :param head_only:
        :param pickcode:
        :param range_header:
        :param sync:
        :return:
        """
        download_info = await self.get_download_info(pickcode)
        url = download_info.url.url
        headers = {"Range": range_header} if range_header else dict()

        method, stream = ("HEAD", False) if head_only else ("GET", True)

        if sync:
            # 返回同步响应
            req = self.sync_client.build_request(method, url, headers=headers)
            r = self.sync_client.send(req, stream=stream)
            return r
        else:
            req = self.async_client.build_request(method, url, headers=headers)
            r = await self.async_client.send(req, stream=stream)
            return r

    async def get_upload_info(self) -> UploadInfo:
        """
        获取用户Key
        :return:
        """
        res = await self.do_request(
            sub_url="", method="GET", schema=UploadInfo, base_url=self.UPLOAD_INFO_URL
        )
        return res

    async def get_token_url(self) -> TokenInfo:
        """
        获取上传信息
        :return:
        """
        params = {"t": int(time.time())}
        res = await self.do_request(
            sub_url="", method="GET", schema=TokenInfo, base_url=self.UPLOAD_TOKEN_URL, params=params
        )
        return res

    async def get_upload_token(self, token_url: str) -> UploadToken:
        params = {"t": int(time.time())}
        res = await self.do_request(
            sub_url="", method="GET", schema=UploadToken, base_url=token_url, params=params
        )
        return res


if __name__ == '__main__':
    pass
